Skip to content

Add @drakkar.software/octobot-client - #3607

Merged
Herklos merged 3 commits into
devfrom
feature/add-sdk
Aug 8, 2026
Merged

Add @drakkar.software/octobot-client#3607
Herklos merged 3 commits into
devfrom
feature/add-sdk

Conversation

@Herklos

@Herklos Herklos commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@Herklos Herklos self-assigned this Aug 1, 2026
@Herklos
Herklos force-pushed the feature/add-sdk branch 3 times, most recently from e75ba4d to 6727a18 Compare August 7, 2026 10:48
Comment on lines +6 to +8
`@drakkar.software/octobot-sdk` (Astrolab's app-internal sync engine) in 2026 — see that repo's
`Astrolab2/frontend/packages/octobot-sdk/CLAUDE.md` for the history and the type-boundary rule that
decided what moved here vs stayed there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove this?

Comment on lines +22 to +27
await client.automations.create({ name: 'My DCA', strategy: dca, accountIds: ['acc1'] })
await client.automations.update('auto1', { name: 'My DCA', strategy: dca, accountIds: ['acc1'] })
await client.automations.stop('auto1')
await client.strategies.create(dca)
await client.strategies.update(dca)
await client.strategies.delete(dca.id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these calls silently failing in readonly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, they don't raise but they return a ProposedAction instead when the client is from connect/readOnly.ts and not a "standard" client from connect/connect.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just updated the tests so that we assert the returned value

@Herklos
Herklos marked this pull request as ready for review August 7, 2026 11:44
@Herklos
Herklos force-pushed the feature/add-sdk branch 6 times, most recently from 6bb54a2 to 2850919 Compare August 7, 2026 21:37
…-only + device-code pairing, space-mirror cloud sync

Squashes this branch's 21 commits into one.

A standalone TypeScript SDK extracted for third-party/website integrations:
wallet derivation (bip44-only, legacy derivation removed entirely — a
derivation-scheme registry replaces the old fixed union so a future wallet
type can register its own scheme), read-only node pairing (scan a QR,
connect read-only, propose actions back to the owning device for
confirmation), and device-code website pairing (a website shows a code, a
human types it into the phone, the phone approves and mints a grant) — the
last one closed two full rounds of adversarial review (rendezvous trust,
replay/freshness, origin validation, clear-and-re-approve recovery), and had
two real bugs fixed in manual code review plus two more found writing an
end-to-end security/functional test suite (a never-written document's null
pull is a JSON string, not a literal; missing null-pull handling entirely).
The retired sealed-data-snapshot pairing exchange (superseded by the
grant-based space-mirror approach below) has been deleted, not just
deprecated.

A running Vite demo (`octobot_client_ts_demo`) exercises every piece above
against the real shared sync server — no in-memory stand-ins. Iterated
several times on honesty/UX: leads with private keys instead of seed
phrases, shares one wallet key across derive/connect/propose instead of
regenerating it, persists the node URL and auto-connects when one exists,
explains the expected placeholder-account failure, and docs were rebuilt
around the demo's own "here's what's real, here's what's fixtures"
structure. This session added a further section proving the paired
website's grant is read-only: `attemptDirectMirrorWrite` makes a raw push
against the mirror space using only the cap+device keys a paired site
already holds, and the demo shows the server reject it — a paired website
cannot unilaterally write, a change has to be requested, approved, and sent
by the user's own device.

Grant-based pairing writes per-collection mirror nodes into a dedicated
Starfish space (replacing the old sealed-snapshot exchange): the mirror
engine runs on `starfish-replica`'s pluggable `ReplicaChannel`/space-mirror
channel, content lives in the generic `objdoc` collection (a short-lived
dedicated `mirrordoc` collection was tried and retired once `objdoc`'s size
limit grew to fit), and a three-value visibility ("private"|"shared"|
"public") now routes each collection to the right storage tier (private ->
E2EE `objdoc`, public -> plaintext world-readable `objpub`) instead of a
binary third-party-eligible flag. The Python-side writer
(`octobot_sync.mirror.writer`) mirrors this on the node.

Moved off a local tarball + pnpm patch for `starfish-replica`/
`starfish-spaces` (workarounds for two alpha.69 bugs — `createNode`'s
invalid self-minted cap, `readSpaces`' stale-while-revalidate race) onto the
published alpha.70 registry versions once both fixes shipped upstream.

This session additionally: fixed `PairingRequestSession` to carry its own
`rendezvous` (was re-spread by hand at every call site) as prep for lifting
the pairing exchange into `starfish-spaces` as a generic primitive; bumped
every `starfish-*` dependency to the published `3.0.0a71`/`3.0.0-alpha.71`
(protocol/keyring/identities/sharing/spaces/sdk); and repointed
`octobot_sync.mirror.writer` off a vendored, stale, buggy fork of
octospaces' Python `dk-spaces-sdk` (predated `starfish-spaces`, byte-identical
storage paths, two real crash bugs, never even wired into the build) onto
the real `starfish-spaces` package directly, deleting the vendored copy
(`packages/dkspaces/`) — see that commit's own detailed rationale for the
full investigation.

Also adds a required `requesterKind: 'website' | 'device'` field to
`PairingRequestPayload` — the device-code exchange was website-shaped but
never actually website-specific, and mobile2 now uses it as a second
requester role (a phone pairing as a read-only viewer of another phone's
cloud mirror). `createPairingRequest`/`startPairingRequest` default it to
`'website'`; `parsePairingRequest` rejects a payload that omits it — no
absent-means-website fallback. Docs/wire-contract/changelog updated.

node_web_interface Settings page: the "Node type" section is hidden (JSX
commented out, not deleted — only "standalone" is actually selectable
today) and Cloud sync moves from its own dedicated card into a small inline
section under Node configuration's "OctoBot interface" block
(`CloudSyncCard.tsx` now exports `CloudSyncSection`, unwrapped from `Card`).
That section's own copy was further simplified: the enabled-state "X of Y
collections" summary box is gone in favor of a bare Configure button (the
count added nothing the modal itself doesn't already show), and the intro
line now says plainly that cloud sync is required for website pairing to
read anything, without the now-redundant "does not affect syncing to your
own node-connected clients" clause.

client: 684 tests, demo: 16 tests, packages/sync: pytest green. All build
clean.

Squashes: 7c6687a, 746eb27, 21d7208, 756cf85, 26b48c8, dcb55be,
5e3efb7, 3255fc4, e9afecc, 2a6b6c6, fdf2e09, 2381c3c, ef8db0e,
265d3a2, 878a5c1, 30af238, 624f8f7, 72feaaf, 0180fab, 01d2e6b,
6727a18. Full pre-squash history preserved at
backup/feature-add-sdk-pre-squash-20260807.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fwj6oBJXJvwSdnEoNqJ3YG
The cloud mirror used to spread a wallet's collections over three spaces
(octobot-mirror, -private, -public). That was not cosmetic: a space keyring is
space-wide and a space:member grant's scope covers spaces/{spaceId}/**, so a
grant over one collection was really a grant over every encrypted node in that
space. Keeping user-settings out of a website grant meant putting it in a
different space entirely.

starfish-replica alpha.72's per-node keyrings (tier: "isolated") remove that
constraint. Every collection now lives in ONE octobot-mirror space, and
visibility decides the tier rather than the space:

  shared  -> isolated  (own per-node keyring, stored in objinv)
  private -> private   (space keyring, objdoc)
  public  -> public    (plaintext, objpub)

objinv rather than objdoc for the grantable tier because objdoc's read roles
are space:member with no cap fallback — a per-node grant holder could never
fetch it. objinv accepts cap:read:objinv, which is exactly the cap
inviteToNode mints. It is declared encryption:"none" server-side, meaning the
server applies no envelope of its own (same as objblob); content is still
E2EE, sealed client-side under the node's keyring.

mintPairingGrant now mints one inviteToNode(..., {isolated: true,
write: false}) per granted collection instead of one inviteToSpace. The
website never joins the space roster, so it cannot read objindex and cannot
enumerate what other collections exist — user-settings is unreachable by
construction rather than by policy. revokePairingGrant rotates each granted
node's keyring, so revoking one collection leaves the others working; the old
grant could only be revoked wholesale.

Breaking on both halves of the wire: the bundle is now
{v:1, spaceId, nodes:[{collectionId, nodeId, contentCap, keyringCap}]} and an
old space-wide bundle is rejected loudly rather than read as empty;
readMirrorCollections takes nodes instead of cap; fetchPairingGrant returns
nodes instead of cap; SyncCloudMirrorResult collapses to one spaceId; and
MIRROR_SPACE_{SHARED,PRIVATE,PUBLIC}_NAME/mirrorSpaceNameFor are replaced by
MIRROR_SPACE_NAME.

Python's mirror/collections.py is updated in lockstep (the TS suite pins
cross-language parity on the registry, tiers and paths) and now routes the
public tier correctly, which it previously did not.

12 new grant tests; 692 pass, typecheck clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fwj6oBJXJvwSdnEoNqJ3YG

@GuillaumeDSM GuillaumeDSM left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great work 👍

Comment on lines +39 to +42
expect(proposed.actions.length).toBeGreaterThan(0)
expect(proposed.payload).toBeDefined()
expect(typeof proposed.payload).toBe('string')
expect(proposed.payload.length).toBeGreaterThan(0)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

The node's cloud-sync settings chain (UI -> REST -> NodeApiService ->
persisted config) was complete and then dead-ended: nothing read those
settings, so enabling cloud sync on a node never made data leave it. The only
writer that actually ran was the TS one, driven by mobile2 and only when no
node is configured.

This wires the Python side, event-triggered rather than periodic.

MirrorService keeps one ChannelScheduler per wallet and one
SpaceMirrorChannel per collection, driven only by sync_now(collection_id). One
channel per collection is what makes that safe: plan_space_mirror filters
existing nodes by the channel's OWN registry, so a single-collection channel
structurally cannot clear its siblings. Verified non-vacuous — the naive
topology (one channel, narrowed enabled_ids) really does wipe the others.

The RW gate falls out for free: a mirror session needs the wallet's private
key, so a caller that cannot resolve one never mirrors. No new Python
read-only-identity machinery was added, because nothing here needs it.

Trigger hook lives on AbstractLocalCollectionProvider so both save paths
(_save_state, save_state) share one implementation. Fire-and-forget and never
raising: a cloud-mirror failure must not fail the local write that caused it.
The starfish_replica import is lazy — it is a full-install dependency, and a
slim install must still be able to write locally.

## Test suite repaired

`packages/sync` is in the CI test matrix and its suite could not even be
collected: `tests/sync/test_collections.py` and `tests/mirror/test_collections.py`
share a basename and neither directory had an `__init__.py`, so pytest raised
"import file mismatch". Added the five missing `__init__.py` files (the tree
already used them elsewhere). 264 -> 262 passing, 0 errors, after the deletions
below.

## Dead code removed

- The hand-rolled writer's mechanics (`sync_cloud_mirror`, `_sync_one_space`,
  `_write_mirror_node`, `_pull_hash_or_none`, ...) and `plan.py` — all
  superseded by starfish_replica.space's channel, all with zero callers.
  writer.py keeps only session construction, which has no upstream twin.
- `mirrordoc_pull_path`, `mirrordoc_push_path`, `is_public_mirror_collection`,
  `is_third_party_eligible`, `DEFAULT_MIRROR_COLLECTIONS` — verified zero
  production callers (grant eligibility is a TS-side concern; this package is
  writer-only). Their tests went with them; the eligibility assertions were
  re-pointed at `is_isolated_mirror_collection`, the live symbol expressing
  the same invariant.

## Broken reader fixed

`_read_user_accounts_trading` called `AccountTradingProvider.list_items()`,
which does not exist on that class — a guaranteed AttributeError, masked by a
bare MagicMock in its test. It now iterates the account list and loads each
account's state, skipping accounts that never traded. It emits
`{account_tradings: [{account_id, account_trading}]}` — the same wire shape
the mobile writer produces, since both feed one mirror and a reader must not
see two shapes.

Comments and docstrings cut hard throughout; the access-model rationale lives
in docs/content/client-sdk/website-pairing.md rather than inline.

Still not fixed, and still default-off: user-data and user-settings have no
local reader. Draft gaps: no coalescing, no teardown, and reconcile does not
clear a just-disabled collection.

262 passed, 1 skipped (env-gated e2e). octobot_client_ts 692 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fwj6oBJXJvwSdnEoNqJ3YG
@Herklos
Herklos merged commit 22e1f9e into dev Aug 8, 2026
22 of 23 checks passed
@Herklos
Herklos deleted the feature/add-sdk branch August 8, 2026 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants